<?php

namespace mongrove;

use IteratorAggregate;
use ArrayAccess;
use ArrayIterator;
use Traversable;

/**
 * The MapField represents a map from a string or integer value to a user definable Field type.
 * Values in the map can be arbitrarily removed, modified and/or added.
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @author Gido Hakvoort <gido@hakvoort.it>
 *
 */
class MapField extends AbstractField implements IteratorAggregate, ArrayAccess, CompositeField {

    /*
     * @var array
     */
    protected $map;

    /*
     * @var array
     */
    protected $unset = array();

    /*
     * @var Field
     */
    protected $field;

    /**
     * Create the definition for the MapField with optional
     * preset values.
     *
     * @param array $default The default values
     */
    public function __construct(array $default = array()) {
        parent :: __construct();

        $this->field = new SimpleField();

        if(isset($default)) {
            $this->setValue($default);
        }
    }

    /*
     * Wrap a value in a copy of the used Field
     */
    protected function toField($value) {
        // TODO : evaluate usage of clone
        $field = clone $this->field;
        $field->setValue($value);

        return $field;
    }

    /**
     * Set the Field type to which contained values must adhere.
     *
     * @param $field Field
     * @return MapField
     */
    public function setField(Field $field) {
        $this->field = $field;

        return $this;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.Field::getValue()
     */
    public function getValue() {
        return $this;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.AbstractField::setValueImpl()
     */
    protected function setValueImpl($value) {
        if($this->map === $value) {
            return false;
        }

        if(!(is_array($value) || ($value instanceof Traversable))) {
            throw new \Exception("{$value} is not of a traversable type");
        }

        $this->map = array();

        foreach($value as $key => $containedValue) {
            $this->map[$key] = $this->toField($containedValue);
        }

        $this->_state |= self :: STATE_NEW;

        return true;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.Field::hydrate()
     */
    public function hydrate($value) {
        $this->map = array();

        foreach($value as $key => $value) {
            $field = clone $this->field;
            $field->hydrate($value);
            $this->map[$key] = $field;
        }

    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.Field::dehydrate()
     */
    public function dehydrate() {
        $result = new \stdClass();

        foreach($this->map as $key => $value) {
            $result->{$key} = $value->dehydrate();
        }

        return $result;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.CompositeField::hasField()
     */
    public function hasField($name) {
        return true;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.CompositeField::getField()
     */
    public function getField($name) {
        return $this->field;
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.Field::getMutations()
     */
    public function getMutations($path = null, $name = null) {
        $path === null ?: $path .= '.';

        /*
         * Require a full dehydrate on full set
         */
        if($this->isNew()) {
            return array(array(Command :: OP_SET => array("{$path}{$name}" => $this->dehydrate())));
        }


        $key = $path ? $path . $name : $name;

        $mutations = array();

        foreach($this->unset as $mapKey => $_) {
            $mutations[0][Command :: OP_UNSET]["{$key}.{$mapKey}"] = 1;
        }

        { // Apply modified internal mutations
            foreach($this->map as $index => $field) {
                $setMutations = $field->getMutations($key, $index);

                foreach($setMutations as $cycle => $cycleMutations) {
                    if(!isset($mutations[$cycle])) {
                        $mutations[$cycle] = $cycleMutations;
                    } else {
                        $mutations[$cycle] = array_merge_recursive($mutations[$cycle], $cycleMutations);
                    }
                }
            }
        }

        return $mutations;

    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.AbstractField::clean()
     */
    public function clean() {
        parent :: clean();

        foreach($this->map as $field) {
            $field->clean();
        }

        $this->unset = array();
    }

    /**
     * (non-PHPdoc)
     * @see src/mongrove.Field::rewriteQuery()
     */
    public function rewriteQuery(array $partialQuery) {
        // TODO check type
        return $partialQuery;
    }

    /*
     * ArrayAccess method implementations
     */

    /**
     * (non-PHPdoc)
     * @see ArrayAccess::offsetExists()
     */
    public function offsetExists($offset) {
        if(!is_scalar($offset)) {
            throw new \Exception('Key is required to be scalar');
        }
        $offset = strval($offset);

        return isset($this->map[$offset]);
    }

    /**
     * (non-PHPdoc)
     * @see ArrayAccess::offsetGet()
     */
    public function offsetGet($offset) {
        if(!is_scalar($offset)) {
            throw new \Exception('Key is required to be scalar');
        }
        $offset = strval($offset);

        $result = $this->map[$offset];

        return $result->getValue();
    }

    /**
     * (non-PHPdoc)
     * @see ArrayAccess::offsetSet()
     */
    public function offsetSet($offset, $value) {
        if(!is_scalar($offset)) {
            throw new \Exception('Key is required to be scalar');
        }

        $offset = strval($offset);

        /*
         * Don't unset that which is set
         */
        if(isset($this->unset[$offset])) {
            unset($this->unset[$offset]);
        }

        if(isset($this->map[$offset])) {
            $this->map[$offset]->setValue($value);
        } else {
            $this->map[$offset] = $this->toField($value);
        }
    }

    /**
     * (non-PHPdoc)
     * @see ArrayAccess::offsetUnset()
     */
    public function offsetUnset($offset) {
        if(!is_scalar($offset)) {
            throw new \Exception('Key is required to be scalar');
        }
        $offset = strval($offset);

        /*
         * Minimize unset actions
         */
        if(isset($this->map[$offset])) {
            $this->unset[$offset] = true;

            unset($this->map[$offset]);
        }
    }

    /*
     * IteratorAggregate
     */

    /**
     * (non-PHPdoc)
     * @see IteratorAggregate::getIterator()
     */
    public function getIterator() {
        return new MapFieldIterator($this->map);
    }

    /**
     * Allow for maps of maps of maps (etc.)
     */
    public function __clone() {
        foreach($this->map as $key => $value) {
            $this->map[$key] = clone $value;
        }
    }
}

/**
 *
 * A modified ArrayIterator for smart iteration over the MapField.
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @author Gido Hakvoort <gido@hakvoort.it>
 *
 */
class MapFieldIterator extends \ArrayIterator {

    /**
     * (non-PHPdoc)
     * @see ArrayIterator::offsetGet()
     */
    public function offsetGet($index) {
        return parent :: offsetGet($index)->getValue();
    }

    /**
     * (non-PHPdoc)
     * @see ArrayIterator::current()
     */
    public function current() {
        return parent :: current()->getValue();
    }
}